/*
 * (C) Copyright 2006-2010 Nuxeo SAS (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     bstefanescu
 */
package org.nuxeo.gwt.habyt.templates.client;

import java.util.ArrayList;
import java.util.List;

import org.nuxeo.gwt.habyt.invocation.client.Actions;
import org.nuxeo.gwt.habyt.invocation.client.ErrorWidget;
import org.nuxeo.gwt.habyt.invocation.client.Invocation;
import org.nuxeo.gwt.habyt.invocation.client.InvocationCallback;
import org.nuxeo.gwt.habyt.invocation.client.InvocationResponse;
import org.nuxeo.gwt.habyt.invocation.client.Invoker;
import org.nuxeo.gwt.habyt.invocation.client.ObjectRef;
import org.nuxeo.gwt.habyt.templates.client.bindings.Helper;
import org.nuxeo.gwt.habyt.templates.client.widgets.HasReplaceElement;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ScriptElement;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.PanelAdapter;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * A panel that contains HTML, and which can attach child widgets to identified
 * elements within that HTML.
 * 
 * The implementation is inspired from {@link HTMLPanel}.
 * 
 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
 * 
 */
public class TemplatePanel extends ComplexPanel implements HasReplaceElement,
        Constants {

    /**
     * The template content is not yet loaded. this class will be removed just
     * after the content is loaded
     */
    private static String EMPTY_TEMPLATE_CLASS = "empty-template";

    public static TemplatePanel wrap(Element element) {
        return wrap(element, null);
    }

    /**
     * Creates a {@link TemplatePanel} widget that wraps an existing &lt;div&gt;
     * element.
     * 
     * This element must already be attached to the document. If the element is
     * removed from the document, you must call
     * {@link RootPanel#detachNow(Widget)}.
     * 
     * @param element the element to be wrapped
     */
    public static TemplatePanel wrap(Element element,
            AsyncCallback<TemplatePanel> cb) {
        // Assert that the element is attached.
        assert Document.get().getBody().isOrHasChild(element);

        TemplatePanel panel = new TemplatePanel(element, cb);

        // Mark it attached and remember it for cleanup.
        panel.onAttach();
        RootPanel.detachOnWindowClose(panel);

        return panel;
    }

    public TemplatePanel() {
        this(true);
    }

    public TemplatePanel(boolean showLoading) {
        setElement(createEmptyDiv(showLoading));
    }

    /**
     * Create a template panel over an existing DOM element
     * 
     * @param element
     */
    protected TemplatePanel(Element element, AsyncCallback<TemplatePanel> cb) {
        setElement(element);
        final LoadingTask task = cb instanceof LoadingTask ? (LoadingTask) cb
                : new LoadingTask(this, cb);
        setHTML(element, task);
    }

    @SuppressWarnings("unchecked")
    public static <T extends Widget> T findWidget(String id) {
        Element element = Document.get().getElementById(id);
        if (element != null) {
            return (T) element.getPropertyObject(THIS);
        }
        return null;
    }

    /**
     * Adds a child widget to the panel, replacing the HTML element.
     * 
     * @param widget the widget to be added
     * @param toReplace the element to be replaced by the widget
     */
    public void addAndReplaceElement(Widget widget, Element toReplace) {
        // Logic pulled from super.add(), replacing the element rather than
        // adding.
        widget.removeFromParent();
        getChildren().add(widget);
        toReplace.getParentNode().replaceChild(widget.getElement(), toReplace);
        adopt(widget);
    }

    protected com.google.gwt.dom.client.Element createEmptyDiv(
            boolean showLoading) {
        com.google.gwt.dom.client.Element el = Document.get().createDivElement();
        if (showLoading) {
            el.addClassName(EMPTY_TEMPLATE_CLASS);
        }
        return el;
    }

    /** template processing methods **/

    public void load(final ObjectRef ref) {
        load(ref, null);
    }

    public void load(final ObjectRef ref, final AsyncCallback<TemplatePanel> cb) {
        load(ref.getInvocation(Actions.VIEW), cb);
    }

    public void load(final Invocation invocation) {
        load(invocation, null);
    }

    public void load(final Invocation invocation,
            final AsyncCallback<TemplatePanel> cb) {
        final LoadingTask task = cb instanceof LoadingTask ? (LoadingTask) cb
                : new LoadingTask(this, cb);
        Invoker.invoke(invocation, new InvocationCallback() {
            public void onSuccess(InvocationResponse result) {
                setHTML((String) result.getContent(), task);
            }

            public void onFailure(Throwable caught) {
                task.onFailure(caught);
            }
        });
    }

    public void setHTML(final String html) {
        setHTML(html, null);
    }

    public void setHTML(final String html, final AsyncCallback<TemplatePanel> cb) {
        final LoadingTask task = cb instanceof LoadingTask ? (LoadingTask) cb
                : new LoadingTask(this, cb);
        Element root = getElement();
        root.setInnerHTML(html);
        setHTML(root, task);
    }

    protected void setHTML(Element root, LoadingTask task) {
        try {
            ArrayList<Binding> bindings = new ArrayList<Binding>();
            loadElement(root, task, bindings);
            for (Binding binding : bindings) {
                binding.bind(this);
            }
            task.onSuccess(this);
        } finally {
            removeStyleName(EMPTY_TEMPLATE_CLASS);
        }
    }

    protected void loadElement(Element root, LoadingTask task,
            List<Binding> bindings) {
        Element node = root.getFirstChildElement();
        while (node != null) {
            Binding binding = null;
            String tag = node.getNodeName().toLowerCase();
            if (tag.startsWith(WIDGET_PREFIX)) {
                // cells are handled by panel widget factories
                if (tag.equals(WIDGET_TEMPLATE)) {
                    // process sub-template
                    Element tplNode = node;
                    node = node.getNextSiblingElement();
                    addAndReplaceElement(createTemplateWidget(task, tplNode),
                            tplNode);
                    continue;
                } else if (!tag.equals(WIDGET_CELL)) {
                    // defer binding after dom is parsed for other widgets
                    String type = tag.substring(WIDGET_PREFIX.length());
                    binding = new Binding(task.getCurrentBinding(), node, type);
                    bindings.add(binding);
                } // else a cell - complex factories will process the cell
            } else if (tag.equals("script")) {
                // scripts must be appended to the dom explicitly
                // otherwise they will not work if inserted through
                // setInnertHTML
                Element scriptNode = node;
                node = node.getNextSiblingElement();
                ScriptElement script = Document.get().createScriptElement();
                String type = Helper.getAttribute(scriptNode, "type");
                if (type != null) {
                    script.setType(type);
                }
                String src = Helper.getAttribute(scriptNode, "src");
                if (src != null && src.length() > 0) {
                    script.setSrc(src);
                } else {
                    script.setText(scriptNode.getInnerText());
                }
                scriptNode.getParentElement().replaceChild(script, scriptNode);
                continue;
            }
            if (node.hasChildNodes()) {
                if (binding != null) {
                    task.pushBinding(binding);
                }
                loadElement(node, task, bindings);
                if (binding != null) {
                    task.popBinding();
                }
                binding = null;
            }
            node = node.getNextSiblingElement();
        }
    }

    protected Widget createLoadingHint() {
        return new HTML("Loading ...");
    }

    protected void handleError(Throwable t) {
        UncaughtExceptionHandler uch = GWT.getUncaughtExceptionHandler();
        if (uch != null) {
            uch.onUncaughtException(t);
        } else {
            Window.alert(t.toString());
        }
    }

    protected Widget createUnknownWidget(String type) {
        return new ErrorWidget("Unknown widget: " + type);
    }

    protected Widget getWidget(String type, Element node) {
        Widget w = WidgetRegistry.get().getWidget(type, node);
        if (w == null) {
            w = createUnknownWidget(type);
        }
        return w;
    }

    static TemplatePanel createTemplateWidget(LoadingTask task, Element element) {
        String id = Helper.getAttribute(element, "id");
        String src = Helper.getAttribute(element, "src");
        if (src == null) {
            src = element.getInnerText().trim();
            if (src.length() == 0) {
                src = null;
            }
        }
        if (src != null) {
            // load remote template
            TemplatePanel w = new TemplatePanel(false);
            if (id != null) {
                w.getElement().setId(id);
            }
            w.load(new ObjectRef(src), task.subtask());
            return w;
        } else {
            throw new IllegalArgumentException(
                    "Invalid w:template - src attribute is required");
        }
    }

    public static class Binding {

        protected WidgetFactory factory;

        protected Element element;

        protected Widget widget;

        protected Binding parent;

        public Binding(Binding parent, Element element, String type) {
            this.element = element;
            this.factory = WidgetRegistry.get().getFactory(type);
            this.parent = parent;
        }

        public Binding getParent() {
            return parent;
        }

        public Element getElement() {
            return element;
        }

        public WidgetFactory getFactory() {
            return factory;
        }

        public Widget getWidget() {
            if (widget == null) {
                if (factory != null) {
                    widget = factory.getWidget(element);
                }
                if (widget == null) { // No such widget found
                    widget = new HTML(
                            "<span style='color:red'>Unknown widget: "
                                    + element.getNodeName().toLowerCase()
                                    + "</span>");
                }
            }
            return widget;
        }

        public void bind(TemplatePanel panel) {
            Widget parentWidget = null;
            while (parent != null) {
                if (parent.getWidget() instanceof HasReplaceElement
                        || parent.getWidget() instanceof ComplexPanel) {
                    parentWidget = parent.getWidget();
                    break;
                }
                parent = parent.parent;
            }
            if (parentWidget == null) {
                panel.addAndReplaceElement(getWidget(), element);
            } else if (parentWidget instanceof HasReplaceElement) {
                ((HasReplaceElement) parentWidget).addAndReplaceElement(
                        getWidget(), element);
            } else if (parentWidget instanceof ComplexPanel) {
                PanelAdapter.addAndReplaceElement((ComplexPanel) parentWidget,
                        getWidget(), element);
            }
        }

    }

}
